StateMachine.js ➔ ... ➔ processResult   B
last analyzed

Complexity

Conditions 6
Paths 16

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
nc 16
dl 0
loc 21
rs 8.7624
c 1
b 0
f 0
nop 2
1
var SDK = require('@ama-team/voxengine-sdk')
2
var Future = SDK.Concurrent.Future
3
var Slf4j = SDK.Logger.Slf4j
4
var Transition = require('./Transition').Transition
5
var Errors = require('../Error')
6
var InternalError = Errors.InternalError
0 ignored issues
show
Comprehensibility introduced by
You are shadowing the built-in type InternalError. This makes code hard to read, consider using a different name.
Loading history...
7
var ScenarioError = Errors.ScenarioError
8
var Schema = require('../Schema')
9
var OperationStatus = Schema.OperationStatus
10
var Normalizer = Schema.Normalizer
11
var Objects = require('../Utility').Objects
12
13
/**
14
 * @param {IExecutor} executor
15
 * @param {TScenario} scenario
16
 * @param {LoggerOptions} [loggerOpts]
17
 *
18
 * @class
19
 */
20
function StateMachine (executor, scenario, loggerOpts) {
21
  // this list contains all unfinished transitions; as soon as transition
22
  // has completed or aborted, it is removed from this list
23
  var transitions = []
24
  var transition = null
25
  var states = scenario.states
26
  var errorHandler = scenario.onError
27
  var state = null
28
  var stage = Stage.Idle
29
  var termination = new Future()
30
  var logger = Slf4j.factory(loggerOpts, 'ama-team.vsf.execution.state-machine')
31
  /**
32
   * @type {TTransitionHistoryEntry[]}
33
   */
34
  var history = []
35
  var entrypoint = Object.keys(states).reduce(function (state, key) {
36
    return state || (states[key].entrypoint ? states[key] : null)
37
  }, null)
38
  if (!entrypoint) {
39
    throw new ScenarioError('No entrypoint state has been defined')
40
  }
41
42
  /**
43
   * @param {StateMachine.Stage} next
44
   */
45
  function setStage (next) {
46
    logger.debug('Changing status from {} to {}', stage.id, next.id)
47
    stage = next
48
  }
49
50
  /**
51
   * Saves current transition status
52
   *
53
   * @param {Transition} t8n
54
   * @param {*} [value] Transition value (if it has finished)
55
   */
56
  function snapshot (t8n, value) {
57
    var origin = t8n.getOrigin()
58
    var entry = {
59
      origin: (origin && origin.id) || null,
60
      target: t8n.getTarget().id,
61
      hints: t8n.getHints(),
62
      status: t8n.getStatus(),
63
      value: value || null
64
    }
65
    history.push(entry)
66
    while (history.length > 100) {
67
      history.shift()
68
    }
69
  }
70
71
  function requireState (id) {
72
    var state = states[id]
73
    if (state) {
74
      return state
75
    }
76
    var msg = 'Could not find requested state ' + id + ' in provided scenario'
77
    throw new ScenarioError(msg)
78
  }
79
80
  /**
81
   * Triggers transition to specified state
82
   *
83
   * @param {TState} target
84
   * @param {THints} hints
85
   */
86
  function transitionTo (target, hints) {
87
    if (stage.terminal) {
88
      var message = 'Can\'t launch new transition from stage ' + stage.id
89
      throw new Errors.IllegalStateError(message)
90
    }
91
    var options = {
92
      logger: loggerOpts,
93
      origin: state,
94
      target: target,
95
      hints: hints || {},
96
      executor: executor
97
    }
98
    return launch(new Transition(options))
99
  }
100
101
  /**
102
   * Aborts current transition (if any)
103
   */
104
  function abort () {
105
    if (!transition) {
106
      return
107
    }
108
    logger.debug('Aborting current transition {}', transition)
109
    transition.abort()
110
    snapshot(transition)
111
    transition = null
112
  }
113
114
  /**
115
   * Launches provided transition, aborting running one (if any) and specifying
116
   * any necessary hooks
117
   *
118
   * @param {Transition} t8n
119
   *
120
   * @return {Thenable}
121
   */
122
  function launch (t8n) {
123
    abort()
124
    transition = t8n
125
    transitions.push(t8n)
126
    snapshot(t8n)
127
    setStage(Stage.Running)
128
    var promise = t8n
129
      .run()
130
      .then(null, function (error) {
131
        logger.error('{} run has rejected', t8n.toString())
132
        return {
133
          value: error,
134
          status: Transition.Stage.Tripped,
135
          duration: (new Date()).getTime() - t8n.getLaunchedAt().getTime()
136
        }
137
      })
138
    promise.then(processResult.bind(null, t8n))
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function processResult declared on line 148 does not use this.
Loading history...
139
    return promise
140
  }
141
142
  /**
143
   * Processes current transition result.
144
   *
145
   * @param {Transition} t8n
146
   * @param {TTransitionResult} result
147
   */
148
  function processResult (t8n, result) {
149
    logger.debug('{} has finished in {} ms', t8n.toString(), result.duration)
150
    setStage(Stage.Idle)
151
    snapshot(t8n, result.value)
152
    var index = transitions.indexOf(t8n)
153
    transitions = index > -1 ? transitions.splice(index, 1) : transitions
154
    var current = t8n === transition
155
    transition = current ? null : transition
156
    if (!current) {
157
      return result
158
    }
159
    var error = result.value
160
    if (result.status.successful) {
161
      try {
162
        return processSuccess(t8n, result.value)
163
      } catch (e) {
164
        error = e
165
      }
166
    }
167
    processError(t8n, error)
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
168
  }
169
170
  /**
171
   * Processes transition success.
172
   *
173
   * @param {Transition} t8n
174
   * @param {*} value
175
   */
176
  function processSuccess (t8n, value) {
177
    logger.debug('{} has resolved with {}, processing', t8n.toString(), value)
178
    value = Normalizer.transition(value)
179
    var destination = t8n.getTarget()
180
    if (value.transitionedTo) {
181
      destination = states[value.transitionedTo]
182
      if (!destination) {
183
        var message = t8n + ' reported transition to state ' +
184
          value.transitionedTo + ' which is not present in scenario states'
185
        throw new ScenarioError(message)
186
      }
187
    }
188
    logger.debug('Transitioned to {}', destination.id)
189
    state = destination
190
    if (state.terminal) {
191
      logger.info('State `{}` is terminal, halting any further processing',
192
        state.id)
193
      terminate(OperationStatus.Finished, value)
194
      return
195
    }
196
    if (!processTrigger(value.trigger || destination.triggers)) {
197
      logger.info('{} didn\'t trigger transition to next state, doing nothing',
198
        t8n)
199
    }
200
  }
201
202
  function processTrigger (trigger) {
203
    logger.trace('Processing trigger {}', trigger)
204
    trigger = Normalizer.stateTrigger(trigger)
205
    if (!trigger || !trigger.id) {
206
      logger.trace('Trigger did not specify transition to next state')
207
      return false
208
    }
209
    var hints = trigger && trigger.hints
210
    hints = Objects.isFunction(hints) ? executor.execute(hints) : hints
211
    transitionTo(requireState(trigger.id), hints)
212
    return true
213
  }
214
215
  /**
216
   * Process transition error
217
   *
218
   * @param {Transition} t8n
219
   * @param {Error|*} error
220
   */
221
  function processError (t8n, error) {
222
    setStage(Stage.ErrorHandling)
223
    if (error instanceof InternalError) {
224
      logger.error('Framework has thrown an error during {}, halting',
225
        t8n.toString())
226
      return terminate(OperationStatus.Tripped, error)
227
    }
228
    logger.error('{} has finished with error, running error handler',
229
      t8n.toString())
230
    var originId = (t8n.getOrigin() && t8n.getOrigin().id) || null
231
    var args = [error, originId, t8n.getTarget().id, t8n.getHints()]
232
    executor
233
      .runHandler(errorHandler, args)
234
      .then(function (value) {
235
        return processTrigger(value && value.trigger)
236
      }, function (e) {
237
        logger.error('Outrageous! Error handler has thrown an error ' +
238
          'itself: {}', e)
239
      })
240
      .then(function (success) {
241
        if (success) {
242
          logger.notice('Error handler has rescued from {} error', t8n.toString())
243
          return
244
        }
245
        terminate(OperationStatus.Failed, error)
246
      })
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
247
  }
248
249
  /**
250
   * Terminates all processing, forbidding new transitions and resolving
251
   * termination as soon as all transitions will finish
252
   *
253
   * @param {OperationStatus} status
254
   * @param {*} [value]
255
   */
256
  function terminate (status, value) {
257
    setStage(Stage.Terminating)
258
    logger.debug('Waiting for {} transitions to finish', transitions.length)
259
    var promises = transitions.map(function (transition) {
260
      var silencer = function () {}
261
      return transition.getCompletion().then(silencer, silencer)
262
    })
263
    Promise.all(promises).then(function () {
264
      setStage(Stage.Terminated)
265
      termination.resolve({
266
        status: status,
267
        value: value || null
268
      })
269
    })
270
  }
271
272
  this.terminate = function () {
273
    if (stage.terminal) {
274
      var message = 'Can not terminate non-active state machine'
275
      throw new Errors.IllegalStateError(message)
276
    }
277
    abort()
278
    terminate(OperationStatus.Aborted, null)
279
    return termination
280
  }
281
282
  /**
283
   * Returns current states
284
   *
285
   * @return {TState}
286
   */
287
  this.getState = function () {
288
    return state
289
  }
290
291
  /**
292
   *
293
   * @return {Transition[]}
294
   */
295
  this.getTransitions = function () {
296
    return transitions.slice()
297
  }
298
299
  /**
300
   *
301
   * @return {Transition}
302
   */
303
  this.getTransition = function () {
304
    return transition
305
  }
306
307
  /**
308
   * @param {TStateId} id
309
   * @param {THints} [hints]
310
   *
311
   * @return {Thenable}
312
   */
313
  this.transitionTo = function (id, hints) {
314
    if (stage.restricted) {
315
      var message = 'State machine is in ' + stage.id + ' state ' +
316
        'and doesn\'t accept #transitionTo() calls'
317
      throw new ScenarioError(message)
318
    }
319
    return transitionTo(requireState(id), hints)
320
  }
321
322
  /**
323
   * Runs state machine
324
   *
325
   * @param {THints} [hints]
326
   * @return {Thenable.<TStateMachineResult>}
327
   */
328
  this.run = function (hints) {
329
    transitionTo(entrypoint, hints)
330
    return termination
331
  }
332
333
  /**
334
   * @return {StateMachine.Stage}
335
   */
336
  this.getStatus = function () {
337
    return stage
338
  }
339
340
  /**
341
   * Returns 100 last transition history events
342
   *
343
   * @return {TTransitionHistoryEntry[]}
344
   */
345
  this.getHistory = function () {
346
    return history
347
  }
348
349
  /**
350
   * Returns termination handle
351
   *
352
   * @return {Thenable.<TStateMachineResult>}
353
   */
354
  this.getTermination = function () {
355
    return termination
356
  }
357
}
358
359
/**
360
 * @typedef {object} StateMachine.Stage~Instance
361
 *
362
 * @property {string} id
363
 * @property {boolean} restricted
364
 * @property {boolean} terminal
365
 */
366
367
/**
368
 * @param {string} id
369
 * @param {boolean} [restricted]
370
 * @param {boolean} [terminal]
371
 * @return {StateMachine.Stage~Instance}
372
 */
373
var stageFactory = function (id, restricted, terminal) {
374
  terminal = typeof terminal === 'boolean' ? terminal : restricted
375
  return {
376
    id: id,
377
    restricted: restricted,
378
    terminal: terminal
379
  }
380
}
381
382
/**
383
 * @enum {StateMachine.Stage~Instance}
384
 * @readonly
385
 */
386
StateMachine.Stage = {
387
  Idle: stageFactory('Idle'),
388
  Running: stageFactory('Running'),
389
  ErrorHandling: stageFactory('ErrorHandling', true, false),
390
  Terminating: stageFactory('Terminating', true),
391
  Terminated: stageFactory('Terminated', true)
392
}
393
394
var Stage = StateMachine.Stage
395
396
module.exports = {
397
  StateMachine: StateMachine
398
}
399